webpack打包 | 您所在的位置:网站首页 › webpack plugin上传oss缺失图片 › webpack打包 |
@TOC# 什么是 webpack? 本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。 webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。 插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。 webpack 通过 Tapable 来组织这条复杂的生产线。 webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。 webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。 – 《深入浅出 webpack》 吴浩麟 webpack 核心概念:Entry:入口起点(entry point)指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。 进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。 每个依赖项随即被处理,最后输出到称之为 bundles 的文件中。 Output:output 属性告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件,默认值为 ./dist。 基本上,整个应用程序结构,都会被编译到你指定的输出路径的文件夹中。Module:模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。 Chunk:代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。 Loader:loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。 本质上,webpack loader 将所有类型的文件,转换为应用程序的依赖图(和最终的 bundle)可以直接引用的模块。 Plugins: loader 被用于转换某些类型的模块,而 plugins(插件)则可以用于执行范围更广的任务。 插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务。 webpack 的核心机制(loader、plugins): Loader工作原理:loader 是用来加载处理各种形式的资源的机制,本质上是一个函数, 接受文件作为参数,返回转化后的结构。 loader 是运行在 NodeJS 中的。因为 webpack 不认识一些外来模块,所以要使用一些加载器,比如识别 css/react/vue/png 等。 loader 虽然是扩展了 webpack ,但是它只专注于转化文件(transform)这一个领域,完成压缩,打包,语言翻译。 例如:css-loader 和 style-loader 模块是为了打包 css 的 babel-loader 和 babel-core 模块时为了把 ES6 的代码转 ES5 url-loader 和 file-loader 是把图片进行打包的。 用 webpack 源码中的代码来理解 loader 的工作原理: 模拟 style-loader 的功能,loader 的简单实现: //将css插入到head标签内部 module.exports = function (source) { let script = (` let style = document.createElement("style"); style.innerText = ${JSON.stringify(source)}; document.head.appendChild(style); `); return script; } //使用方式1 resolveLoader: { modules: [ path.resolve('node_modules'), path.resolve(__dirname, 'src', 'loaders')] }, { test: /\.css$/, use: ['style-loader'] }, } //使用方式2 //将自己写的loaders发布到npm仓库,然后添加到依赖,按照方式1中的配置方式使用即可 以下代码是 webpack 源码中 loader 执行关键步骤,以递归的方式执行 loader,执行机制流程似于 express 中间件机制: function iteratePitchingLoaders(options, loaderContext, callback) { var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex]; // load loader module loadLoader(currentLoaderObject, function (err) { var fn = currentLoaderObject.pitch; runSyncOrAsync( fn, loaderContext, [ loaderContext.remainingRequest, loaderContext.previousRequest, (currentLoaderObject.data = {}), ], function (err) { if (err) return callback(err); var args = Array.prototype.slice.call(arguments, 1); if (args.length > 0) { loaderContext.loaderIndex--; iterateNormalLoaders(options, loaderContext, args, callback); } else { iteratePitchingLoaders(options, loaderContext, callback); } } ); }); } plugin 工作原理:plugin 是一个具有 apply 方法的 js 对象。 apply 方法会被 webpack 的 compiler(编译器)对象调用,并且 compiler 对象可在整个 compilation(编译)生命周期内访问。 plugins 是作用于 webpack 本身上的。webpack 提供了很多开箱即用的插件 插件可以携带参数,所以可以在 plugins 属性传入 new 实例: 1)CommonChunkPlugin 主要用于提取第三方库和公共模块,避免首屏加载的 bundle 文件,或者按需加载的 bundle 文件体积过大,导致加载时间过长,是一把 优化的利器。而在多页面应用中,更是能够为每个页面间的应用程序共享代码创建 bundle。 2)针对 html 文件打包和拷贝(还有很多设置)的插件:html-webpack-plugin 其不但完成了 html 文件的拷贝,打包,还给 html 中自动增加了引入打包后的 js 文件的代码(),还能指明把 js 文 件引入到 html 文件的底部等等。 代码如下: plugins: [ // 对html模板进行处理,生成对应的html,引入需要的资源模块 new HtmlWebpackPlugin({ template: "./index.html", // 模板文件,即需要打包和拷贝到build目录下的html文件 filename: "index.html", // 目标html文件 chunks: ["useperson"], // 对应加载的资源,即html文件需要引入的js模块 inject: true, // 资源加入到底部,把模块引入到html文件的底部 }), ]; webpack 中 plugins 的组成: 一个 JavaScript 函数或者 class(ES6 语法)。在它的原型上定义一个 apply 方法。指定挂载的 webpack 事件钩子。处理 webpack 内部实例的特定数据。功能完成后调用 webpack 提供的回调。以常用的插件 UglifyJsPlugin 为分析示例: class UglifyJsPlugin { apply(compiler) { const options = this.options; options.test = options.test || /\.js($|\?)/i; ...... //绑定compilation事件 compiler.plugin("compilation", (compilation) => { if(options.sourceMap) { compilation.plugin("build-module", (module) => { // to get detailed location info about errors module.useSourceMap = true; }); } //绑定optimize-chunk-assets事件 compilation.plugin("optimize-chunk-assets", (chunks, callback) => { const files = []; chunks.forEach((chunk) => files.push.apply(files, chunk.files)); ...... callback(); }); }); } } module.exports = UglifyJsPlugin; webpack 的核心构建流程:这个过程核心完成了 内容转换 + 资源合并 两种功能,在实现上包含三个阶段: 1、初始化阶段:初始化参数:从配置文件、 配置对象、Shell 参数中读取,与默认配置结合得出最终的参数 创建编译器对象:用上一步得到的参数创建 Compiler 对象 初始化编译环境:包括注入内置插件、注册各种模块工厂、初始化 RuleSet 集合、加载配置的插件等 开始编译:执行 compiler 对象的 run 方法 确定入口:根据配置中的 entry 找出所有的入口文件,调用 compilition.addEntry 将入口文件转换为 dependence 对象 2、构建阶段:编译模块(make):根据 entry 对应的 dependence 创建 module 对象,调用 loader 将模块转译为标准 JS 内容,调用 JS 解释器将内容转换为 AST 对象,从中找出该模块依赖的模块,再 递归 本步骤直到所有入口依赖的文件都经过了本步骤的处理 完成模块编译:上一步递归处理所有能触达到的模块后,得到了每个模块被翻译后的内容以及它们之间的 依赖关系图 3、生成阶段:输出资源(seal):根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会 写入文件系统(emitAssets):在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统 可理解为 Webpack 的运行流程是一个串行的过程,从启动到结束依次执行的流程 如下: 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译。 确定入口:根据配置中的 entry 找出所有的入口文件。 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。 完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统 看图加深理解 webpack 打包流程,如下图所示: 图示流程理解分析: 读取入口文件;基于 AST(抽象语法树) 分析入口文件,并产出依赖列表;AST (Abstract Syntax Tree)抽象语法树 在计算机科学中,或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。 (https://astexplorer.net/)使用 Babel 将相关模块编译到 ES5;webpack 有一个智能解析器(各种 babel),几乎可以处理任何第三方库。无论它们的模块形式是 CommonJS、AMD 还是普通的 JS 文件;甚至在加载依赖的时候,允许使用动态表 require(“、/templates/”+name+“、jade”)。以下这些工具底层依赖了不同的解析器生成 AST,比如 eslint 使用了 espree、babel 使用了 acorn对每个依赖模块产出一个唯一的 ID,方便后续读取模块相关内容;将每个依赖以及经过 Babel 编译过后的内容,存储在一个对象中进行维护;遍历上一步中的对象,构建出一个依赖图(Dependency Graph);将各模块内容 bundle 产出 个人理解 webpack 是是 npm 的工具模块,是一个 JS 应用打包器, 它将应用中的各个模块打包成一个或者多个 bundle 文件。 借助 loaders 和 plugins,它可以改变、压缩和优化各种各样的文件。 输入不同资源,比如:html、css、js、img、font 文件等,然后将它们输出浏览器可以正常解析的文件。以上就是我对 webpack 的简单理解,但要理解 webpack 到底是什么,一定要弄清楚下面两个词: 模块化打包 什么是模块化?1.模块化开发是一种管理方式,是一种生产方式,一种解决问题的方案。他按照功能将一个软件切分成许多部分单独开发,然后再组装起来,每一个部分即为模块。 2.当使用模块化开发的时候可以避免刚刚的问题,并且让开发的效率变高,以及方便后期的维护。 为什么需要模块化?1.现今的很多网页其实可以看做是功能丰富的应用,它们拥有着复杂的 JavaScript 代码和一大堆依赖包。 2.当一个项目开发的越来越复杂的时候,你会遇到一些问题:命名冲突(变量和函数命名可能相同),文件依赖(引入外部的文件数目、顺序问题)等。 3.JavaScript 发展的越来越快,超过了它产生时候的自我定位。这时候 js 模块化就出现了 模块化进程: 发展一:早期:script 标签这是最原始的 JavaScript 文件加载方式,如果把每一个文件看做是一个模块,那么他们的接口通常是暴露在全局作用域下,也就是定义在 window 对象中。 缺点:1.污染全局作用域 2.只能按 script 标签书写顺序加载 3.文件依赖关系靠开发者主观解决 发展一:CommonJS 规范 允许模块通过 require 方法来同步加载(同步意味阻塞)所要依赖的其他模块,然后通过 module.exports 来导出需要暴露的接口。 // module add.js module.exports = function add (a, b) { return a + b; } // main.js var {add} = require('./math'); console.log('1 + 2 = ' + add(1,2);CommonJS 是以在浏览器环境之外构建 JavaScript 生态系统为目标而产生的项目,比如在服务器和桌面环境中。 发展二:AMD/CMD(1)AMD AMD 是 RequireJS 在推广过程中对模块定义的规范化产出(异步模块定义)。 AMD 标准中定义了以下两个 API: require([module], callback); define(id, [depends], callback); require接口用来加载一系列模块,define接口用来定义并暴露一个模块。 define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好 a.add1() ... b.add2() ... })优点: 1、适合在浏览器环境中异步加载模块 2、可以并行加载多个模块 (2)CMD CMD 是 SeaJS 在推广过程中对模块定义的规范化产出。(在 CommomJS 和 AMD 基础上提出) define(function (requie, exports, module) { //依赖可以就近书写 var a = require('./a'); a.add1(); ... if (status) { var b = requie('./b'); b.add2(); } }); 优点: 依赖就近,延迟执行 可以很容易在服务器中运行(3)AMD 和 CMD 的区别: AMD 和 CMD 起来很相似,但是还是有一些细微的差别: 1.对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。 2.AMD 推崇依赖前置;CMD 推崇依赖就近,只有在用到某个模块的时候再去 require。 3、AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一 发展三:ES6 模块(ESModule) ECMAScript 2015标准增加了JavaScript语言层面的模块体系定义(关键字)。 在 ES6 中,我们使用export关键字来导出模块,使用import关键字引用模块。 // module math.jsx export default class Math extends React.Component {} // main.js import Math from "./Math"; ES6 模块与 CommonJS 模块的差异:CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。 CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。 即 CommonJS 先加载整个模块,输出一个对象,取对象内相应的值,输出后内部不会再变化;ES6 是静态编译命令,先加载一个引用,等执行时再根据引用到加载模块内取值输出,动态引用不缓存。 目前只有很少的 JS 引擎能直接支持 ES6 标准,因此 Babel 的做法实际上是将不被支持的 import 翻译成目前已被支持的 require。 发展四:后模块化的编译时代由于目前很少JS引擎能直接支持 ES6 标准,但是为了让我们的新代码也能运行在用户的老浏览器中,社区涌现了越来越多的工具,他们能静态将高版本规范的代码编译为低版本规范的代码,最为大家所熟知的就是babel。 Babel 的做法可以将不被支持的 import 翻译成目前已被支持的 require。 它把 JS Core 中高版本规范的语法,也能按照相同语义在静态阶段转化成为低版本的语法,这样即使是早期的浏览器,他们内置的 JS 解释器也能看懂。 然而不幸的是,对于模块化相关的import和export关键字,babel最终会将它编译为包含require和exports的CommonJS规范。 这就造成了另一个问题,这样带有模块化关键词的模块,编译之后还是没办法直接运行在浏览器中,因为浏览器端并不能运行 CommonJS 的模块。所以编译这一步并不能帮我们解决,模块化通用的问题。 那么,我们该怎么解决模块化通用的问题呢? 一来,我们怎么把 ESModule 里面的 import 和 export 运用在各个地方; 二来,就是如何让我们在各个地方共同的使用我们的 AMD、CommonJS 等的模块化规范,那我们需要的步骤就是打包。 所以,为了能在 WEB 端直接使用 CommonJS 规范的模块, 除了编译(babel)之外我们还需要一个步骤叫做 打包(bundle) 二、打包 模块化打包工具:webpack Rollup Parcel fis 打包工具要解决的问题:文件依赖管理 梳理文件之间的依赖关系 资源加载管理 处理文件的加载顺序(先后时机)和文件的加载数量(合并、嵌入、拆分) 效率与优化管理 提高开发效率,完成页面优化 webpack 打包的规则:一个入口文件对应一个 bundle。该 bundle 包括入口文件模块和其依赖的模块。 按需加载的模块或需单独加载的模块则分开打包成其他的 bundle。 除了这些 bundle 外,还有一个特别重要的 bundle,就是 manifest.bundle.js 文件,即 webpackBootstrap。 这个 manifest 文件是最先加载的,负责解析 webpack 打包的其他 bundle 文件,使其按要求进行加载和执行。 webpack 底层是如何处理打包的? 1. 参考 Node.js 源码来熟悉 CommonJS 的处理方式 我们可以参考Node.js对于CommonJS模块的处理方式来处理一个CommonJS模块。 在Node.js中,所有的CommonJS模块都会被包裹在一个函数中,然后在node.js中使用vm(虚拟机,http://nodejs.cn/api/vm.html)来运行它,最终达到一个模块化导入和导出的目的。 好比我们执行了 node index.js 执行的时候,node会通过文件系统读取index.js里面的内容,这时候是一个字符串,同时对这个字符串进行一个包裹。通过一个函数字符串的形式,将这个文件的内容包裹进去。把它变成了一个字符串的函数。 1. 首先,当它加载进来一个模块之后,它确定我们要执行哪个commonJS模块之后,node会通过文件系统(fs)读取index.js里面的内容,会在上面和下面加入函数字符串,这样在里面就可以使 用require和exports了,变成了一个函数,也有了参数。 2. 然后,将字符串变成可执行的函数,很多种方式eval、new Function之类的,但node中直接调用vm的模块,这个模块和fs、path一样是一个内置模块。作用和new Function、eval类型,就 是把字符串变成可执行的函数。Node中将字符串放入runInNewContent或者runInThisContent之类的方法就可以变成一个可以执行的函数。 3. 同时,注入进去require和exports等的内容。 4. 之后,就可以在模块之间进行导入和导出了。 **这就是Node.js中如何进行CommonJS操作的流程。**上代码: const str = `require('./moduleA'); const str = require('./moduleB'); console.log(str);`; const functionWrapper = ["function(require, module, exports) {", "}"]; // 将我们的文件进行包裹,成为一个字符串函数 const result = functionWrapper[0] + str + functionWrapper[1]; const vm = require("vm"); vm.runInNewContext();比较难想到的(Node.js中比较核心的就是这一步): 如何将 require、exports 注入进每一个模块。如何将 CommonJS 模块变成一个可执行文件这个是比较难想到的。VM 模块就是调用 V8 相关的接口将我们的字符串变成一个真正可执行的函数。如何将一个 CommonJS 的模块变成一个可执行的函数呢?从而把他们执行呢?其实就是 VM 这一层做的。 2. 浏览器中对 CommonJS 的处理我们在浏览器中也可以用相同的思路进行处理: 我们在打包阶段将每个模块包裹上一层函数字符串,然后放置到浏览器中去执行它。 同时我们实现一个简单版本的 require 函数和 module 对象来处理运行时加载的问题,这样一个基本流程就好了。 接下来我们要处理运行时模块之间的依赖关系,所以我们需要自己维护一个。 我们要做一个东西,怎么把 CommonJS 的规范运行在浏览器里面去? 逆推的思路来想这个事情。怎么结合刚才讲的 CommonJS 模块的原理。 思考一下,假如我们要把这个 index.js 模块放在浏览器里面运行,需要做哪些东西呢? 我们可以先手动写 bundle 文件: 思路: 首先,放到作用域里面去,要解决变量提升、函数提升等作用域冲突。需要先定义一个自执行函数,函数作用域是比较稳定的; 然后,我们仿照 CommonJS 的步骤,对模块进行包裹。将我们 index.js 的模块用函数包裹的形式包裹起来,让它出现在打包的结果里面,这样至少执行的时候不会报错了,因为注入了变量。 接下来,是我们怎么注入变量,之前说到我们要实现 require 函数和 module.exports 对象,我们先实现一个 module 对象吧,同时里面有 exports 方法。 而 require 函数是加载模块用的,实际接收一个 id,通过 id 去找其他模块。 结合用 webpack 打包后的 bundle.js 内的代码,举例来验证: 文件目录结构: 代码块 (function (modules) { // 打包成了一个自执行函数 var installedModules = {} // 缓存 function __webpack_require__(moduleId) { // 模拟了一个require方法 原理:通过递归的方式不停的调用自己 if (installedModules[moduleId]) { return installedModules[moduleId].exports } var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {} } modules[moduleId].call(module.exports, module, module.exports, __webpack_require__) module.l = true return module.exports } // return __webpack_require_((__webpack_require_.s = "./main.js")) return __webpack_require__(0) })({ // 0 key:index.js value: 是一个函数 "./index.js": (function (module, exports) { eval( 'import a from "./a";\n\nconsole.log("hello word");\n\n\n//#sourceURL=webpack:///./index.js?' ), "./a.js": function (module,exports) { eval( '// import b from "./b";\n\nconsole.log("hello word");\n\n\n//#sourceURL=webpack:///./index.js?' ), "./c.js": function (module,exports) { eval( '// \n\nconsole.log("hello word");\n\n\n//#sourceURL=webpack:///./index.js?' ), "./d.js": function (module,exports) { eval( '// \n\nconsole.log("hello word");\n\n\n//#sourceURL=webpack:///./index.js?' ), }, "./b.js": function (module,exports) { eval( '// import c from "./c";\n\nconsole.log("hello word");\n\n\n//#sourceURL=webpack:///./index.js?' )} }) })它借助了一个__webpack_require函数来实现自己的模块化,把代码都存放在installedModules,代码文件以对象形式传递进来,key 是文件的路径(需要打包的文件),value是一个函数,通过eval()执行当前文件的代码。value可以理解为:包裹代码的字符串,并且代码内部的require,都被替换成了__webpack_require__。 咱们来分析下上述代码的运行机制: 打包出来的 bundle.js 是一个 IIFE (立即调用函数表达式)modules 是一个对象,每个 key 对应的是一个模块函数函数 webpack_require 加载模块,返回 module.exportswebpack 中每个模块都有一个唯一的 id,是从 0 开始递增的,即从入口文件开始。通过 webpack_require(0) 启动程序我们实际上就是把 index.js 里面的这个模块用函数包裹了一下,然后 mock 了一个 module,它就可以运行了。模块内容其实是没有变化的,只是包裹在了一个函数里面,同时执行了它。 node index.bundle.js 执行成功,然后放到浏览器里面执行也可以,这个时候说明这个模块他就是一个环境无关的代码了,经过我们的这么一个处理之后,就不用再关心它有没有 module.exports 这种 CommonJS 规范了。 咱们对这个立即执行函数进行简单的理解: (闭包函数)(以入口文件为首的需要打包的文件们) 对闭包函数部分进行分析: 首先它接收一个 id,同时通过闭包的形式把 currentModuleId 也传入进去,这样就能让每一个 require 函数都知道是由哪一个模块进入这个模块的,最终返回结果。 这个闭包的作用就是当我 require index.js 的时候,我应该去 modules 里面的哪个下标来去找这个对应关系总结: webpack 支持所有符合 ES5 标准 的浏览器(不支持 IE8 及以下版本)。 对 Webpack 的使用者来说,它是一个简单强大的工具,对 Webpack 的开发者来说,它是一个扩展性的高系统。 Webpack 之所以能成功,在于它把复杂的实现隐藏了起来,给用户暴露出的只是一个简单的工具,让用户能快速达成目的。 同时整体架构设计合理,扩展性高,开发扩展难度不高,通过社区补足了大量缺失的功能,让 Webpack 几乎能胜任任何场景。 Webpack 是一个庞大的 Node.js 应用,如果你阅读过它的源码,你会发现实现一个完整的 Webpack 需要编写非常多的代码。但你无需了解所有的细节,只需了解其整体架构和部分细节即可。 Node.js 从最一开始就支持模块化编程。然而,在 web,模块化的支持正缓慢到来。在 web 存在多种支持 JavaScript 模块化的工具,这些工具各有优势和限制。webpack 基于从这些系统获得的经验教训,并将模块的概念应用于项目中的任何文件。 |
CopyRight 2018-2019 实验室设备网 版权所有 |